We've learned how to display data, and pass data as props. What we haven't learned is how to manipulate data! This is where state comes in.
xxxxxxxxxx111const handleAction = someEvent => {2 console.log("Some event : ", someEvent);3};45const Child = props => {6 return <button onClick={props.onAction}> Click Me! </button>;7};89export const Parent = () => {10 return <Child />;11};Suppose we wanted to start counting upward when clicking the child's button. There are two ways that you'll see as an approach to solving this problem:
We're not going to spend too much time on classes, but since they're not deprecated, we're definitely going to talk about them for a brief moment. For reference to what's happening, the following code is from ParentClassComponent.jsx in the example-state project:
xxxxxxxxxx521import React from "react";2import { Child } from "../Child";345// Instead of defining our component functionally like we've done before, 6// we define our component as a class extending `React.Component`7export class ParentComponent extends React.Component {89 // We need a constructor to build our component (which takes in our props)10 constructor(props) {11 super(props); // and then call our superconstructor. 1213 // We assign our state variables by using `this.state`. 14 // Any key within state's object will be accessible15 this.state = {16 timesPressed: 017 };18 19 // When we want to have functions access state, we need to bind them like so20 this.handleAction = this.handleAction.bind(this);21 }2223 24 // Here is a handle action function that we'll send to our child component. 25 handleAction(action) {26 console.log("Handling the action! ", action);2728 // To manipulate state, we can use `this.setState`,29 // and pass in a new updated state object 30 // (here, we're adding one to our timesPressed)31 this.setState({32 timesPressed: this.state.timesPressed + 133 });3435 console.log("TIMES PRESSED? ", this.state.timesPressed);36 }3738 39 // And below, we need a way to work with our JSX, so we use the `render` function.40 // You can put anything inside this function (i.e. anything before the return),41 // it just needs to return JSX at the end. 42 render() {4344 return (45 <>46 <Child onClick={this.handleAction} />47 <div>That dang button got clicked {this.state.timesPressed} times </div>48 </>49 );50 }51}52
Regardless of whether or not you use multiple of the same component, they will each have their own state. Try replacing <ParentComponent /> in the ReactDOM.render with <ParentChildParty /> in the index.js file.
xxxxxxxxxx241//... above code removed23const ParentChildParty = () => {4 return (5 <>6 <h1>Parent 1</h1>7 <ParentComponent />8 <br />9 <h1>Parent 2</h1>10 <ParentComponent />11 <br />12 <h1>Parent 3</h1>13 <ParentComponent />14 <br />15 <div>16 Clearly, every single instance of "ParentClassComponent" has its own17 state. Try opening the devtools and looking around18 </div>19 </>20 );21};2223ReactDOM.render(<ParentComponent />, document.querySelector("#root"));24Each ParentComponent has its own state that is accessed individually!
Asynchronicity, not a word, but it sounds cool, and setState is an asynchronous function. Let's try adding a console log to read from state immediately after we set it:
xxxxxxxxxx91 2 handleAction(action) {3 this.setState({4 timesPressed: this.state.timesPressed + 15 });67 console.log("TIMES PRESSED? ", this.state.timesPressed);8 }9 What ends up happening is that we wind up getting a console log in our browser that's logging the previous state, while our browser itself is rendering what the actual state is. For example, suppose this.state.timesPressed was 2, if we called handAction, we'll then asynchronously call setState, and then move onto console.log, where 2 will get printed to the console while state is being updated to 3!
To keep your code clean, you can just as easily pass a function to your set state that returns a state object of whatever you wish to update. The function by default takes in (state, props). That is, it takes in the previous state and props (prior to being updated). You can see an example of this in ParentClassWithStateFunction.jsx in the corresponding code.
xxxxxxxxxx1312 addOneToState(state, props) {3 console.log("State?", state);4 console.log("Props???", props);5 return {6 timesPressed: this.state.timesPressed + 17 };8 }910 handleAction(action) {11 this.setState(this.addOneToState);12 }13
More State Functions
You can just as easily create other methods that work on the same state variables, that is, it's somewhat pointless having only a single function that works on a state variable. You'll often want to have multiple functions referencing the same state variables to tie your app together, such as a reset function for our button counter (an example can be seen in the ParentClassWithResetFunction.jsx file:
xxxxxxxxxx4212 constructor(props) {3 super(props);4 this.state = {5 timesPressed: 06 };78 this.handleAction = this.handleAction.bind(this);910 // MAKE SURE TO BIND YOUR ADDITIONAL FUNCTIONS! 11 // This will constantly turn around and bite you (it does for me)12 this.resetTimesPressed = this.resetTimesPressed.bind(this);13 }1415 addOneToState() {16 console.log("inside add1 to state");17 return {18 timesPressed: this.state.timesPressed + 119 };20 }2122 // added resetStateToZero function23 // this just sends a `timesPressed: 0` object to state!24 resetStateToZero() {25 console.log("inside reset state to 0");26 return {27 timesPressed: 028 };29 }3031 handleAction(action) {32 console.log("Handling the action! ", action);3334 this.setState(this.addOneToState);3536 console.log("TIMES PRESSED? ", this.state.timesPressed);37 }3839 resetTimesPressed(action) {40 this.setState(this.resetStateToZero);41 }42
REITERATING: In order to make sure that you don't find yourself in a pickle (i.e. if you use a class component with constructors), make sure you always bind your functions!
(Try removing a this.<functionName> = this.<functionName>.bind(this) from the constructor)
What happens if you have more than one element in your state? You likely will want more than one element in your state, such as a text field (example can be seen in ParentClassMultipleStateItems.jsx):
xxxxxxxxxx521export class ParentComponent extends React.Component {2 constructor(props) {3 console.log("This instatiates the component and brings in props", props);4 super(props);5 this.state = {6 timesPressed: 0,7 value: ""8 };910 this.handleAction = this.handleAction.bind(this);11 this.resetTimesPressed = this.resetTimesPressed.bind(this);12 13 // BINDING THE NEW FUNCTION THAT WILL HANDLE OUR TYPING IN THE TEXT FIELD14 this.handleTypeAction = this.handleTypeAction.bind(this);15 }1617 // Removed handleAction/ResetTimesPressed/addOneToState/resetStateToZero for space18 19 // UPDATING THE VALUE (just like with addOneToState and resetStateToZero)20 updateTheValue(event) {21 return {22 value: event.target.value23 };24 }2526 // Handling the type action! 27 // Notice that we're passing event into this.updateTheView28 // which is returning an object, and then being passed into setState? 29 // First class functions rule! 30 handleTypeAction(event) {31 this.setState(this.updateTheValue(event));32 }3334 35 // Below, we have our text box in our render function! 36 render() {37 return (38 <>39 <Child onClick={this.handleAction} />40 <div>That dang button got clicked {this.state.timesPressed} times </div>41 <div>42 <input43 type="text"44 value={this.state.value}45 onChange={this.handleTypeAction}46 />47 </div>48 <GrandChild onClick={this.resetTimesPressed} />49 </>50 );51 }52}
When you have more and more complex states, you'll often have compartmentalized data in the form of an object, or an array, etc (may as a user object with a username and password?). When updating your state, I'm sure you've noticed that you only insert the portion of state that you'd like to update. This works great at the top level. That's because state is updated with a shallow copy. If you try to do this with objects in state, it will not deep copy. Take a look at the below code (working components in ParentComponentShallowDeepState.jsx). When we update username or password, we're not passing in its corresponding value (that is, on username, we're not also passing in password, and vice versa).
Try running the below code. It doesn't necessarily make the most sense for the text boxes (since they don't rerender the values), so we have the username and password displaying within a div after each:
xxxxxxxxxx761export class ParentComponent extends React.Component {2 constructor(props) {3 console.log("This instatiates the component and brings in props", props);4 super(props);5 this.state = {6 timesPressed: 0,7 value: "",8 user: {9 username: "",10 password: ""11 }12 };1314 this.handleAction = this.handleAction.bind(this);15 this.resetTimesPressed = this.resetTimesPressed.bind(this);16 this.handleUsernameAction = this.handleUsernameAction.bind(this);17 this.handlePasswordAction = this.handlePasswordAction.bind(this);18 }1920 // handle times pressed/reset with corresponding functionsremoved2122 updateUsername(event) {23 return {24 user: {25 username: event.target.value26 }27 };28 }2930 updatePassword(event) {31 return {32 user: {33 password: event.target.value34 }35 };36 }37 handleUsernameAction(event) {38 this.setState(this.updateUsername(event));39 console.log("State after setting username", this.state);40 }4142 handlePasswordAction(event) {43 this.setState(this.updatePassword(event));44 console.log("State after setting password", this.state);45 }4647 render() {48 return (49 <>50 <Child onClick={this.handleAction} />51 <div>That dang button got clicked {this.state.timesPressed} times </div>52 <div>53 Username:54 <input55 type="text"56 value={this.state.user.username}57 onChange={this.handleUsernameAction}58 />59 <div> Username typed: {this.state.user.username} </div>60 </div>61 <br />62 <br />63 <div>Password:</div>64 <input65 type="password"66 value={this.state.user.password}67 onChange={this.handlePasswordAction}68 />69 <div> Password typed: {this.state.user.password} </div>7071 <br />72 <GrandChild onClick={this.resetTimesPressed} />73 </>74 );75 }76}
I'm sure you noticed that the second you started typing in password, the username went away (and the same goes for username). To avoid this problem, you only need to either pass in the part of state you wish to keep:
xxxxxxxxxx1912updateUsername(event) {3 return {4 user: {5 username: event.target.value,6 password: this.state.user.password7 }8 };9}1011updatePassword(event) {12 return {13 user: {14 password: event.target.value,15 username: this.state.user.username16 }17 };18}19
However, it's pretty likely that you'll have more than just two keys within your object, so you can use the spread operator to get around that and spread whatever object you'd like (in our case, it's this.state.user):
xxxxxxxxxx191...2updateUsername(event) {3 return {4 user: {5 ...this.state.user,6 username: event.target.value,7 }8 };9}1011updatePassword(event) {12 return {13 user: {14 ...this.state.user,15 password: event.target.value,16 }17 };18}19...
What's happening above is that we're essentially copying this.state.user and then overwriting a specific key. CAVEAT: Make sure you spread your object first. If you spread it last, then you'll be overwriting your new data with your old data! Go to ParentComponentShallowDeepState.jsx to see a working example. Try placing the the spread after your updated values. What do you see?
We've seen that we can update and manipulate our state with the setState methods, but our components are getting incredibly large. Keeping file sizes below a hundred lines is definitely nice to do. Let's clean our code up a little bit. We can start with making use of arrow functions! Because arrow functions don't bind to their own this and move up a level, they implicitly bind to the class itself (so we don't have to remember all of those this.whatever = this.whatever.bind(this)).
So now we can remove the constructor entirely, set our state by just creating a state variable and turning our functions into arrow functions (seen in ParentClassClean.jsx)!
xxxxxxxxxx881import React from "react";2import { Child } from "../Child";3import { GrandChild } from "../GrandChild";45export class ParentComponent extends React.Component {6 // get rid of that constructor7 state = {8 timesPressed: 0,9 value: "",10 user: {11 username: "",12 password: ""13 }14 };1516 // change all 17 addOneToState = () => ({18 timesPressed: this.state.timesPressed + 119 });2021 resetStateToZero = () => ({22 timesPressed: 023 });2425 updateUsername = event => ({26 user: {27 username: event.target.value,28 password: this.state.user.password29 }30 });3132 updatePassword = event => {33 return {34 user: {35 password: event.target.value,36 username: this.state.user.username37 }38 };39 }4041 handleAction = action => {42 this.setState(this.addOneToState);43 };4445 resetTimesPressed = action => {46 this.setState(this.resetStateToZero);47 };4849 handleUsernameAction = event => {50 this.setState(this.updateUsername(event));51 console.log("State after setting username", this.state);52 };5354 handlePasswordAction = event => {55 this.setState(this.updatePassword(event));56 console.log("State after setting password", this.state);57 };5859 render() {60 return (61 <>62 <Child onClick={this.handleAction} />63 <div>That dang button got clicked {this.state.timesPressed} times </div>64 <div>65 Username:66 <input67 type="text"68 value={this.state.user.username}69 onChange={this.handleUsernameAction}70 />71 <div> Username typed: {this.state.user.username} </div>72 </div>73 <br />74 <br />75 <div>Password:</div>76 <input77 type="password"78 value={this.state.user.password}79 onChange={this.handlePasswordAction}80 />81 <div> Password typed: {this.state.user.password} </div>8283 <br />84 <GrandChild onClick={this.resetTimesPressed} />85 </>86 );87 }88}